package simple;

import asset.AssetGetter;
import asset.AssetGetterFinder;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import simple.util.FTPUtil;

import java.io.*;
import java.net.ConnectException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * 此加载器是一个java扩展加载器，它可以加载本机的jar，挂在http协议上的远程jar，挂在ftp协议上的远程jar
 * 若要加载ftp的jar，请调用setFtpExtract(FTPExtract ftpExtract)方法，重写user、password以及jarRoot的获取方法
 * 若jar会依赖其它外部资源，针对ftp协议，只需要将其与jar放在同一个目录下即可，获取其输入流时，仅需要通过文件名即可获取
 *                       针对http协议，则需要调用setAssetLoader(AssetLoader assetLoader)方法，重写AssetLoader的loadAsset()方法
 *                       加载器在运行远程的jar之前，会调用AssetLoader的loadAsset方法收集jar所需的所有资源。
 *                       重写AssetLoader的keyExist(String key)方法，处理key重复时的策略。
 */
public class RemoteClassLoader extends ClassLoader implements AssetGetter {

    private URL[] remotePaths;

    private Map<String, Class<?>> classes;

    private List<byte[]> bytes;

    private Map<String, byte[]> asset;

    private FTPExtract ftpExtract = new FTPExtract() {
        @Override
        public String getUser() {
            return "";
        }

        @Override
        public String getPassword() {
            return "";
        }

        @Override
        public List<String> getJarPath() {
            return new ArrayList<>();
        }
    };

    private AssetLoader assetLoader = new AssetLoader() {
        @Override
        public Map<String, byte[]> loadAsset() {
            return new HashMap<>();
        }

        @Override
        public void keyExist(String key) {
            throw new IllegalArgumentException("出现重复的键：".concat(key));
        }
    };

    public RemoteClassLoader(URL[] remotePaths) throws URISyntaxException, IOException {
        super();
        this.remotePaths = remotePaths;
        classes = new HashMap<>();
        this.bytes = new ArrayList<>();
        this.asset = new HashMap<>();
        AssetGetterFinder.setAssetGetter(this);
        for (URL remotePath : remotePaths) {
            String protocol = remotePath.getProtocol();
            if (protocol.equals("file")) {
                File file = new File(remotePath.toURI());
                if (!file.isFile()) {
                    throw new FileNotFoundException("找不到对应的jar：".concat(file.getAbsolutePath()));
                }
                try (FileInputStream fileInputStream = new FileInputStream(new File(remotePath.toURI()))) {
                    byte[] bytes = new byte[fileInputStream.available()];
                    fileInputStream.read(bytes);
                    this.bytes.add(bytes);
                }
            } else if (protocol.equals("ftp")) {
                String path = remotePath.getPath();
                String[] split = path.split(":");
                String ip = split[0];
                int port;
                if (path.endsWith("/")) {
                    port = Integer.parseInt(split[1].substring(0, split[1].length() - 1));
                } else {
                    port = Integer.parseInt(split[1]);
                }
                String user = ftpExtract.getUser();
                String password = ftpExtract.getPassword();
                List<String> jarPath = ftpExtract.getJarPath();
                if (jarPath == null || jarPath.isEmpty()) {
                    continue;
                }
                FTPClient ftpClient = FTPUtil.getFTPClient(ip, port, user, password);
                // 递归记载所有jar
                for (String jar : jarPath) {
                    int count = jar.split("/").length;
                    loadFTP(ftpClient, jar);
                    for (int i = 0; i < count; i++) {
                        ftpClient.changeToParentDirectory();
                    }
                }
            } else {
                try {
                    Object content = remotePath.getContent();
                    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                        ioCopy((InputStream) content, byteArrayOutputStream);
                        this.bytes.add(byteArrayOutputStream.toByteArray());
                    }
                } catch (ConnectException e) {
                    System.err.println(remotePath.getPath().concat(" ").concat(e.getMessage()));
                }
            }
        }

        Map<String, byte[]> stringMap = assetLoader.loadAsset();
        for (Map.Entry<String, byte[]> entry : stringMap.entrySet()) {
            if (this.asset.containsKey(entry.getKey())){
                assetLoader.keyExist(entry.getKey());
            }
            this.asset.put(entry.getKey(), entry.getValue());
        }
    }

    // 加载jar
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (classes.containsKey(name)) {
            return classes.get(name);
        }
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }
        InputStream inputStream = getResourceAsStream(name.replace(".", "/").concat(".class"));
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        if (inputStream == null) {
            return super.loadClass(name, resolve);
        }
        try {
            ioCopy(inputStream, byteArrayOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        byte[] data = byteArrayOutputStream.toByteArray();
        Class<?> aClass = defineClass(name, data, 0, data.length);
        if (resolve) {
            resolveClass(aClass);
        }
        this.classes.put(name, aClass);
        return aClass;
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        for (byte[] aByte : bytes) {
            try {
                JarInputStream jarInputStream = new JarInputStream(new ByteArrayInputStream(aByte));
                JarEntry jarEntry;
                while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
                    if (jarEntry.getName().equals(name)) {
                        return jarInputStream;
                    }
                }
                jarInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    // 将输入流的数据，写到输出流中
    private void ioCopy(InputStream inputStream, OutputStream outputStream) throws IOException {
        byte[] cache = new byte[8 * 1024];
        int index = -1;
        while ((index = inputStream.read(cache)) != -1) {
            outputStream.write(cache, 0, index);
        }
    }

    public FTPExtract getFtpExtract() {
        return ftpExtract;
    }

    public void setFtpExtract(FTPExtract ftpExtract) {
        this.ftpExtract = ftpExtract;
    }

    public AssetLoader getAssetLoader() {
        return assetLoader;
    }

    public void setAssetLoader(AssetLoader assetLoader) {
        this.assetLoader = assetLoader;
    }

    // 下载远程的FTP jar
    private void loadFTP(FTPClient ftpClient, String rootDir) throws IOException {
        ftpClient.enterLocalPassiveMode();
        boolean b = FTPUtil.changeDir(ftpClient, rootDir, "/");
        if (!b) {
            return;
        }
        FTPFile[] ftpFiles = ftpClient.listFiles();
        for (FTPFile ftpFile : ftpFiles) {
            if (ftpFile.isFile()) {
                // 下载
                String remote = new String(ftpFile.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                boolean flag = ftpClient.retrieveFile(remote, byteArrayOutputStream);

                if (flag) {
                    if (remote.endsWith(".jar")) {
                        this.bytes.add(byteArrayOutputStream.toByteArray());
                    } else {
                        this.asset.put(remote, byteArrayOutputStream.toByteArray());
                    }
                    System.out.println("ftp依赖加载成功！");
                } else {
                    System.err.println("ftp依赖加载失败！");
                }

                byteArrayOutputStream.close();
            } else {
                String name = ftpFile.getName();
                loadFTP(ftpClient, name);
            }
        }
        ftpClient.changeToParentDirectory();
    }

    @Override
    public InputStream findAsset(String name) {
        if (this.asset.containsKey(name)) {
            return new ByteArrayInputStream(this.asset.get(name));
        }
        return null;
    }

    /**
     * FTP扩展接口
     */
    public interface FTPExtract {

        /**
         * 重写此方法获取ftp登录用户名
         * @return ftp登录名称
         */
        String getUser();

        /**
         * 重写此方法获取ftp登录密码
         * @return ftp登录密码
         */
        String getPassword();

        /**
         * 重写此方法获取ftp中jar的根路径
         * @return jar包路径管理容器
         */
        List<String> getJarPath();
    }

    /**
     * jar资源收集器
     */
    public interface AssetLoader {

        /**
         * 重写此方法定制所有jar的资源收集器
         * @return 资源的管理容器
         */
        Map<String, byte[]> loadAsset();

        /**
         * 重写此方法，在合并资源时出现重名key时，处理重名问题
         * @param key 已经存在的key
         */
        void keyExist(String key);
    }
}
